Implement `cargo:rerun-if-env-changed=FOO`
authorAlex Crichton <alex@alexcrichton.com>
Mon, 5 Jun 2017 14:52:31 +0000 (07:52 -0700)
committerAlex Crichton <alex@alexcrichton.com>
Wed, 14 Jun 2017 21:23:15 +0000 (14:23 -0700)
This commit implements a new method of rerunning a build script if an
environment variable changes. Environment variables are one of the primary
methods of giving inputs to a build script today, and this'll help situations
where if you change an env var you don't have to remember to clean out an old
build directory to ensure fresh results.

Closes #2776

src/cargo/ops/cargo_compile.rs
src/cargo/ops/cargo_rustc/context.rs
src/cargo/ops/cargo_rustc/custom_build.rs
src/cargo/ops/cargo_rustc/fingerprint.rs
src/doc/build-script.md
tests/build-script-env.rs [new file with mode: 0644]

index 888939a22034681caad923abd02718cdaf573c71..f6fc2af94e51e2ea9dac70bd2a8aae45e4e5e3e7 100644 (file)
@@ -703,6 +703,7 @@ fn scrape_target_config(config: &Config, triple: &str)
             env: Vec::new(),
             metadata: Vec::new(),
             rerun_if_changed: Vec::new(),
+            rerun_if_env_changed: Vec::new(),
             warnings: Vec::new(),
         };
         // We require deterministic order of evaluation, so we must sort the pairs by key first.
@@ -745,7 +746,9 @@ fn scrape_target_config(config: &Config, triple: &str)
                         output.env.push((name.clone(), val.to_string()));
                     }
                 }
-                "warning" | "rerun-if-changed" => {
+                "warning" |
+                "rerun-if-changed" |
+                "rerun-if-env-changed" => {
                     bail!("`{}` is not supported in build script overrides", k);
                 }
                 _ => {
index 1b1242f90385725c15c62d33594cbb7f959a2258..91d24d054e49be7d5db3638b359dc22b0b10b8cd 100644 (file)
@@ -17,7 +17,7 @@ use util::{self, internal, Config, profile, Cfg, CfgExpr};
 use util::errors::{CargoResult, CargoResultExt};
 
 use super::TargetConfig;
-use super::custom_build::{BuildState, BuildScripts};
+use super::custom_build::{BuildState, BuildScripts, BuildDeps};
 use super::fingerprint::Fingerprint;
 use super::layout::Layout;
 use super::links::Links;
@@ -38,7 +38,7 @@ pub struct Context<'a, 'cfg: 'a> {
     pub compilation: Compilation<'cfg>,
     pub packages: &'a PackageSet<'cfg>,
     pub build_state: Arc<BuildState>,
-    pub build_explicit_deps: HashMap<Unit<'a>, (PathBuf, Vec<String>)>,
+    pub build_explicit_deps: HashMap<Unit<'a>, BuildDeps>,
     pub fingerprints: HashMap<Unit<'a>, Arc<Fingerprint>>,
     pub compiled: HashSet<Unit<'a>>,
     pub build_config: BuildConfig,
index 6bb2620fdaed95743970bd702405d155d24b4ccb..15c149bcc14d6144b568e00e248e7987100f7368 100644 (file)
@@ -28,6 +28,8 @@ pub struct BuildOutput {
     pub metadata: Vec<(String, String)>,
     /// Paths to trigger a rerun of this build script.
     pub rerun_if_changed: Vec<String>,
+    /// Environment variables which, when changed, will cause a rebuild.
+    pub rerun_if_env_changed: Vec<String>,
     /// Warnings generated by this build,
     pub warnings: Vec<String>,
 }
@@ -59,6 +61,12 @@ pub struct BuildScripts {
     pub plugins: BTreeSet<PackageId>,
 }
 
+pub struct BuildDeps {
+    pub build_script_output: PathBuf,
+    pub rerun_if_changed: Vec<String>,
+    pub rerun_if_env_changed: Vec<String>,
+}
+
 /// Prepares a `Work` that executes the target as a custom build script.
 ///
 /// The `req` given is the requirement which this run of the build script will
@@ -181,11 +189,8 @@ fn build_work<'a, 'cfg>(cx: &mut Context<'a, 'cfg>, unit: &Unit<'a>)
     // Check to see if the build script has already run, and if it has keep
     // track of whether it has told us about some explicit dependencies
     let prev_output = BuildOutput::parse_file(&output_file, &pkg_name).ok();
-    let rerun_if_changed = match prev_output {
-        Some(ref prev) => prev.rerun_if_changed.clone(),
-        None => Vec::new(),
-    };
-    cx.build_explicit_deps.insert(*unit, (output_file.clone(), rerun_if_changed));
+    let deps = BuildDeps::new(&output_file, prev_output.as_ref());
+    cx.build_explicit_deps.insert(*unit, deps);
 
     fs::create_dir_all(&script_output)?;
     fs::create_dir_all(&build_output)?;
@@ -246,8 +251,6 @@ fn build_work<'a, 'cfg>(cx: &mut Context<'a, 'cfg>, unit: &Unit<'a>)
 
         })?;
 
-        paths::write(&output_file, &output.stdout)?;
-        paths::write(&err_file, &output.stderr)?;
 
         // After the build command has finished running, we need to be sure to
         // remember all of its output so we can later discover precisely what it
@@ -256,6 +259,8 @@ fn build_work<'a, 'cfg>(cx: &mut Context<'a, 'cfg>, unit: &Unit<'a>)
         // This is also the location where we provide feedback into the build
         // state informing what variables were discovered via our script as
         // well.
+        paths::write(&output_file, &output.stdout)?;
+        paths::write(&err_file, &output.stderr)?;
         let parsed_output = BuildOutput::parse(&output.stdout, &pkg_name)?;
 
         if json_messages {
@@ -337,6 +342,7 @@ impl BuildOutput {
         let mut env = Vec::new();
         let mut metadata = Vec::new();
         let mut rerun_if_changed = Vec::new();
+        let mut rerun_if_env_changed = Vec::new();
         let mut warnings = Vec::new();
         let whence = format!("build script of `{}`", pkg_name);
 
@@ -378,6 +384,7 @@ impl BuildOutput {
                 "rustc-env" => env.push(BuildOutput::parse_rustc_env(value, &whence)?),
                 "warning" => warnings.push(value.to_string()),
                 "rerun-if-changed" => rerun_if_changed.push(value.to_string()),
+                "rerun-if-env-changed" => rerun_if_env_changed.push(value.to_string()),
                 _ => metadata.push((key.to_string(), value.to_string())),
             }
         }
@@ -389,6 +396,7 @@ impl BuildOutput {
             env: env,
             metadata: metadata,
             rerun_if_changed: rerun_if_changed,
+            rerun_if_env_changed: rerun_if_env_changed,
             warnings: warnings,
         })
     }
@@ -436,6 +444,20 @@ impl BuildOutput {
     }
 }
 
+impl BuildDeps {
+    pub fn new(output_file: &Path, output: Option<&BuildOutput>) -> BuildDeps {
+        BuildDeps {
+            build_script_output: output_file.to_path_buf(),
+            rerun_if_changed: output.map(|p| &p.rerun_if_changed)
+                                    .cloned()
+                                    .unwrap_or_default(),
+            rerun_if_env_changed: output.map(|p| &p.rerun_if_env_changed)
+                                        .cloned()
+                                        .unwrap_or_default(),
+        }
+    }
+}
+
 /// Compute the `build_scripts` map in the `Context` which tracks what build
 /// scripts each package depends on.
 ///
index a3fc7ffe574ff6d1d44d9b3c2819611934901df7..44f227f2a2c86077ca3773cec3460a05d80af624 100644 (file)
@@ -1,3 +1,4 @@
+use std::env;
 use std::fs::{self, File, OpenOptions};
 use std::hash::{self, Hasher};
 use std::io::prelude::*;
@@ -18,6 +19,7 @@ use util::paths;
 
 use super::job::Work;
 use super::context::{Context, Unit};
+use super::custom_build::BuildDeps;
 
 /// A tuple result of the `prepare_foo` functions in this module.
 ///
@@ -136,7 +138,7 @@ pub struct Fingerprint {
     profile: u64,
     #[serde(serialize_with = "serialize_deps", deserialize_with = "deserialize_deps")]
     deps: Vec<(String, Arc<Fingerprint>)>,
-    local: LocalFingerprint,
+    local: Vec<LocalFingerprint>,
     #[serde(skip_serializing, skip_deserializing)]
     memoized_hash: Mutex<Option<u64>>,
     rustflags: Vec<String>,
@@ -160,7 +162,7 @@ fn deserialize_deps<'de, D>(d: D) -> Result<Vec<(String, Arc<Fingerprint>)>, D::
             rustc: 0,
             target: 0,
             profile: 0,
-            local: LocalFingerprint::Precalculated(String::new()),
+            local: vec![LocalFingerprint::Precalculated(String::new())],
             features: String::new(),
             deps: Vec::new(),
             memoized_hash: Mutex::new(Some(hash)),
@@ -173,25 +175,33 @@ fn deserialize_deps<'de, D>(d: D) -> Result<Vec<(String, Arc<Fingerprint>)>, D::
 enum LocalFingerprint {
     Precalculated(String),
     MtimeBased(MtimeSlot, PathBuf),
+    EnvBased(String, Option<String>),
 }
 
 struct MtimeSlot(Mutex<Option<FileTime>>);
 
 impl Fingerprint {
     fn update_local(&self) -> CargoResult<()> {
-        match self.local {
-            LocalFingerprint::MtimeBased(ref slot, ref path) => {
-                let meta = fs::metadata(path)
-                    .chain_err(|| {
-                        internal(format!("failed to stat `{}`", path.display()))
-                    })?;
-                let mtime = FileTime::from_last_modification_time(&meta);
-                *slot.0.lock().unwrap() = Some(mtime);
+        let mut hash_busted = false;
+        for local in self.local.iter() {
+            match *local {
+                LocalFingerprint::MtimeBased(ref slot, ref path) => {
+                    let meta = fs::metadata(path)
+                        .chain_err(|| {
+                            internal(format!("failed to stat `{}`", path.display()))
+                        })?;
+                    let mtime = FileTime::from_last_modification_time(&meta);
+                    *slot.0.lock().unwrap() = Some(mtime);
+                }
+                LocalFingerprint::EnvBased(..) |
+                LocalFingerprint::Precalculated(..) => continue,
             }
-            LocalFingerprint::Precalculated(..) => return Ok(())
+            hash_busted = true;
         }
 
-        *self.memoized_hash.lock().unwrap() = None;
+        if hash_busted {
+            *self.memoized_hash.lock().unwrap() = None;
+        }
         Ok(())
     }
 
@@ -220,32 +230,47 @@ impl Fingerprint {
         if self.rustflags != old.rustflags {
             return Err(internal("RUSTFLAGS has changed"))
         }
-        match (&self.local, &old.local) {
-            (&LocalFingerprint::Precalculated(ref a),
-             &LocalFingerprint::Precalculated(ref b)) => {
-                if a != b {
-                    bail!("precalculated components have changed: {} != {}",
-                          a, b)
+        if self.local.len() != old.local.len() {
+            bail!("local lens changed");
+        }
+        for (new, old) in self.local.iter().zip(&old.local) {
+            match (new, old) {
+                (&LocalFingerprint::Precalculated(ref a),
+                 &LocalFingerprint::Precalculated(ref b)) => {
+                    if a != b {
+                        bail!("precalculated components have changed: {} != {}",
+                              a, b)
+                    }
                 }
-            }
-            (&LocalFingerprint::MtimeBased(ref on_disk_mtime, ref ap),
-             &LocalFingerprint::MtimeBased(ref previously_built_mtime, ref bp)) => {
-                let on_disk_mtime = on_disk_mtime.0.lock().unwrap();
-                let previously_built_mtime = previously_built_mtime.0.lock().unwrap();
-
-                let should_rebuild = match (*on_disk_mtime, *previously_built_mtime) {
-                    (None, None) => false,
-                    (Some(_), None) | (None, Some(_)) => true,
-                    (Some(on_disk), Some(previously_built)) => on_disk > previously_built,
-                };
-
-                if should_rebuild {
-                    bail!("mtime based components have changed: previously {:?} now {:?}, \
-                           paths are {:?} and {:?}",
-                          *previously_built_mtime, *on_disk_mtime, ap, bp)
+                (&LocalFingerprint::MtimeBased(ref on_disk_mtime, ref ap),
+                 &LocalFingerprint::MtimeBased(ref previously_built_mtime, ref bp)) => {
+                    let on_disk_mtime = on_disk_mtime.0.lock().unwrap();
+                    let previously_built_mtime = previously_built_mtime.0.lock().unwrap();
+
+                    let should_rebuild = match (*on_disk_mtime, *previously_built_mtime) {
+                        (None, None) => false,
+                        (Some(_), None) | (None, Some(_)) => true,
+                        (Some(on_disk), Some(previously_built)) => on_disk > previously_built,
+                    };
+
+                    if should_rebuild {
+                        bail!("mtime based components have changed: previously {:?} now {:?}, \
+                               paths are {:?} and {:?}",
+                              *previously_built_mtime, *on_disk_mtime, ap, bp)
+                    }
                 }
+                (&LocalFingerprint::EnvBased(ref akey, ref avalue),
+                 &LocalFingerprint::EnvBased(ref bkey, ref bvalue)) => {
+                    if *akey != *bkey {
+                        bail!("env vars changed: {} != {}", akey, bkey);
+                    }
+                    if *avalue != *bvalue {
+                        bail!("env var `{}` changed: previously {:?} now {:?}",
+                              akey, bvalue, avalue)
+                    }
+                }
+                _ => bail!("local fingerprint type has changed"),
             }
-            _ => bail!("local fingerprint type has changed"),
         }
 
         if self.deps.len() != old.deps.len() {
@@ -359,7 +384,7 @@ fn calculate<'a, 'cfg>(cx: &mut Context<'a, 'cfg>, unit: &Unit<'a>)
         profile: util::hash_u64(&unit.profile),
         features: format!("{:?}", cx.resolve.features_sorted(unit.pkg.package_id())),
         deps: deps,
-        local: local,
+        local: vec![local],
         memoized_hash: Mutex::new(None),
         rustflags: extra_flags,
     });
@@ -403,36 +428,7 @@ pub fn prepare_build_cmd<'a, 'cfg>(cx: &mut Context<'a, 'cfg>, unit: &Unit<'a>)
 
     debug!("fingerprint at: {}", loc.display());
 
-    // If this build script execution has been overridden, then the fingerprint
-    // is just a hash of what it was overridden with. Otherwise the fingerprint
-    // is that of the entire package itself as we just consider everything as
-    // input to the build script.
-    let (local, output_path) = {
-        let state = cx.build_state.outputs.lock().unwrap();
-        match state.get(&(unit.pkg.package_id().clone(), unit.kind)) {
-            Some(output) => {
-                let s = format!("overridden build state with hash: {}",
-                                util::hash_u64(output));
-                (LocalFingerprint::Precalculated(s), None)
-            }
-            None => {
-                let &(ref output, ref deps) = &cx.build_explicit_deps[unit];
-
-                let local = if deps.is_empty() {
-                    let s = pkg_fingerprint(cx, unit.pkg)?;
-                    LocalFingerprint::Precalculated(s)
-                } else {
-                    let deps = deps.iter().map(|p| unit.pkg.root().join(p));
-                    let mtime = mtime_if_fresh(output, deps);
-                    let mtime = MtimeSlot(Mutex::new(mtime));
-                    LocalFingerprint::MtimeBased(mtime, output.clone())
-                };
-
-                (local, Some(output.clone()))
-            }
-        }
-    };
-
+    let (local, output_path) = build_script_local_fingerprints(cx, unit)?;
     let mut fingerprint = Fingerprint {
         rustc: 0,
         target: 0,
@@ -454,17 +450,19 @@ pub fn prepare_build_cmd<'a, 'cfg>(cx: &mut Context<'a, 'cfg>, unit: &Unit<'a>)
     // necessary for that fingerprint.
     //
     // Hence, if there were some `rerun-if-changed` directives forcibly change
-    // the kind of fingerprint over to the `MtimeBased` variant where the
-    // relevant mtime is the output path of the build script.
+    // the kind of fingerprint by reinterpreting the dependencies output by the
+    // build script.
     let state = cx.build_state.clone();
     let key = (unit.pkg.package_id().clone(), unit.kind);
+    let root = unit.pkg.root().to_path_buf();
     let write_fingerprint = Work::new(move |_| {
         if let Some(output_path) = output_path {
             let outputs = state.outputs.lock().unwrap();
-            if !outputs[&key].rerun_if_changed.is_empty() {
-                let slot = MtimeSlot(Mutex::new(None));
-                fingerprint.local = LocalFingerprint::MtimeBased(slot,
-                                                                 output_path);
+            let outputs = &outputs[&key];
+            if !outputs.rerun_if_changed.is_empty() ||
+               !outputs.rerun_if_env_changed.is_empty() {
+                let deps = BuildDeps::new(&output_path, Some(outputs));
+                fingerprint.local = local_fingerprints_deps(&deps, &root);
                 fingerprint.update_local()?;
             }
         }
@@ -474,6 +472,62 @@ pub fn prepare_build_cmd<'a, 'cfg>(cx: &mut Context<'a, 'cfg>, unit: &Unit<'a>)
     Ok((if compare.is_ok() {Fresh} else {Dirty}, write_fingerprint, Work::noop()))
 }
 
+fn build_script_local_fingerprints<'a, 'cfg>(cx: &mut Context<'a, 'cfg>,
+                                             unit: &Unit<'a>)
+    -> CargoResult<(Vec<LocalFingerprint>, Option<PathBuf>)>
+{
+    let state = cx.build_state.outputs.lock().unwrap();
+    // First up, if this build script is entirely overridden, then we just
+    // return the hash of what we overrode it with.
+    //
+    // Note that the `None` here means tha twe don't want to update the local
+    // fingerprint afterwards because this is all just overridden.
+    if let Some(output) = state.get(&(unit.pkg.package_id().clone(), unit.kind)) {
+        debug!("override local fingerprints deps");
+        let s = format!("overridden build state with hash: {}",
+                        util::hash_u64(output));
+        return Ok((vec![LocalFingerprint::Precalculated(s)], None))
+    }
+
+    // Next up we look at the previously listed dependencies for the build
+    // script. If there are none then we're in the "old mode" where we just
+    // assume that we're changed if anything in the packaged changed. The
+    // `Some` here though means that we want to update our local fingerprints
+    // after we're done as running this build script may have created more
+    // dependencies.
+    let deps = &cx.build_explicit_deps[unit];
+    let output = deps.build_script_output.clone();
+    if deps.rerun_if_changed.is_empty() && deps.rerun_if_env_changed.is_empty() {
+        debug!("old local fingerprints deps");
+        let s = pkg_fingerprint(cx, unit.pkg)?;
+        return Ok((vec![LocalFingerprint::Precalculated(s)], Some(output)))
+    }
+
+    // Ok so now we're in "new mode" where we can have files listed as
+    // dependencies as well as env vars listed as dependencies. Process them all
+    // here.
+    Ok((local_fingerprints_deps(deps, unit.pkg.root()), Some(output)))
+}
+
+fn local_fingerprints_deps(deps: &BuildDeps, root: &Path) -> Vec<LocalFingerprint> {
+    debug!("new local fingerprints deps");
+    let mut local = Vec::new();
+    if !deps.rerun_if_changed.is_empty() {
+        let output = &deps.build_script_output;
+        let deps = deps.rerun_if_changed.iter().map(|p| root.join(p));
+        let mtime = mtime_if_fresh(output, deps);
+        let mtime = MtimeSlot(Mutex::new(mtime));
+        local.push(LocalFingerprint::MtimeBased(mtime, output.clone()));
+    }
+
+    for var in deps.rerun_if_env_changed.iter() {
+        let val = env::var(var).ok();
+        local.push(LocalFingerprint::EnvBased(var.clone(), val));
+    }
+
+    return local
+}
+
 fn write_fingerprint(loc: &Path, fingerprint: &Fingerprint) -> CargoResult<()> {
     let hash = fingerprint.hash();
     debug!("write fingerprint: {}", loc.display());
index 6de0ab9117fe403464e20c4f73e5c46fe1aeaee0..a0bfe0fd669ca3a92391b1d8ee12affa4b433ace 100644 (file)
@@ -97,6 +97,15 @@ crate is built:
   then it's rebuilt and rerun unconditionally, so
   `cargo:rerun-if-changed=build.rs` is almost always redundant (unless you
   want to ignore changes in all other files except for `build.rs`).
+* `rerun-if-env-changed=VAR` is the name of an environment variable which
+  indicates that if the environment variable's value changes the build script
+  should be rerun. This basically behaves the same as `rerun-if-changed` except
+  that it works with environment variables instead. Note that the environment
+  variables here are intended for global environment variables like `CC` and
+  such, it's not necessary to use this for env vars like `TARGET` that Cargo
+  sets. Also note that if `rerun-if-env-changed` is printed out then Cargo will
+  *only* rerun the build script if those environment variables change or if
+  files printed out by `rerun-if-changed` change.
 
 * `warning=MESSAGE` is a message that will be printed to the main console after
   a build script has finished running. Warnings are only shown for path
diff --git a/tests/build-script-env.rs b/tests/build-script-env.rs
new file mode 100644 (file)
index 0000000..1230de3
--- /dev/null
@@ -0,0 +1,106 @@
+extern crate cargotest;
+extern crate hamcrest;
+
+use std::fs::File;
+
+use cargotest::sleep_ms;
+use cargotest::support::{project, execs};
+use hamcrest::assert_that;
+
+#[test]
+fn rerun_if_env_changes() {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.5.0"
+            authors = []
+        "#)
+        .file("src/main.rs", r#"
+            fn main() {}
+        "#)
+        .file("build.rs", r#"
+            fn main() {
+                println!("cargo:rerun-if-env-changed=FOO");
+            }
+        "#);
+    p.build();
+
+    assert_that(p.cargo("build"),
+                execs().with_status(0)
+                       .with_stderr("\
+[COMPILING] foo v0.5.0 ([..])
+[FINISHED] [..]
+"));
+    assert_that(p.cargo("build").env("FOO", "bar"),
+                execs().with_status(0)
+                       .with_stderr("\
+[COMPILING] foo v0.5.0 ([..])
+[FINISHED] [..]
+"));
+    assert_that(p.cargo("build").env("FOO", "baz"),
+                execs().with_status(0)
+                       .with_stderr("\
+[COMPILING] foo v0.5.0 ([..])
+[FINISHED] [..]
+"));
+    assert_that(p.cargo("build").env("FOO", "baz"),
+                execs().with_status(0)
+                       .with_stderr("\
+[FINISHED] [..]
+"));
+    assert_that(p.cargo("build"),
+                execs().with_status(0)
+                       .with_stderr("\
+[COMPILING] foo v0.5.0 ([..])
+[FINISHED] [..]
+"));
+}
+
+#[test]
+fn rerun_if_env_or_file_changes() {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.5.0"
+            authors = []
+        "#)
+        .file("src/main.rs", r#"
+            fn main() {}
+        "#)
+        .file("build.rs", r#"
+            fn main() {
+                println!("cargo:rerun-if-env-changed=FOO");
+                println!("cargo:rerun-if-changed=foo");
+            }
+        "#)
+        .file("foo", "");
+    p.build();
+
+    assert_that(p.cargo("build"),
+                execs().with_status(0)
+                       .with_stderr("\
+[COMPILING] foo v0.5.0 ([..])
+[FINISHED] [..]
+"));
+    assert_that(p.cargo("build").env("FOO", "bar"),
+                execs().with_status(0)
+                       .with_stderr("\
+[COMPILING] foo v0.5.0 ([..])
+[FINISHED] [..]
+"));
+    assert_that(p.cargo("build").env("FOO", "bar"),
+                execs().with_status(0)
+                       .with_stderr("\
+[FINISHED] [..]
+"));
+    sleep_ms(1000);
+    File::create(p.root().join("foo")).unwrap();
+    assert_that(p.cargo("build").env("FOO", "bar"),
+                execs().with_status(0)
+                       .with_stderr("\
+[COMPILING] foo v0.5.0 ([..])
+[FINISHED] [..]
+"));
+}